using System;
using System.IO;

using Org.BouncyCastle.Utilities.IO;

namespace Org.BouncyCastle.Bcpg
{
	/// <remarks>Basic output stream.</remarks>
	public class BcpgOutputStream
		: BaseOutputStream
	{
		internal static BcpgOutputStream Wrap(
			Stream outStr)
		{
			if (outStr is BcpgOutputStream)
			{
				return (BcpgOutputStream) outStr;
			}

			return new BcpgOutputStream(outStr);
		}

		private Stream outStr;
		private byte[] partialBuffer;
		private int partialBufferLength;
		private int partialPower;
		private int partialOffset;
		private const int BufferSizePower = 16; // 2^16 size buffer on long files

		/// <summary>Create a stream representing a general packet.</summary>
		/// <param name="outStr">Output stream to write to.</param>
		public BcpgOutputStream(
			Stream outStr)
		{
			if (outStr == null)
				throw new ArgumentNullException("outStr");

			this.outStr = outStr;
		}

		/// <summary>Create a stream representing an old style partial object.</summary>
		/// <param name="outStr">Output stream to write to.</param>
		/// <param name="tag">The packet tag for the object.</param>
		public BcpgOutputStream(
			Stream		outStr,
			PacketTag	tag)
		{
			if (outStr == null)
				throw new ArgumentNullException("outStr");

			this.outStr = outStr;
			this.WriteHeader(tag, true, true, 0);
		}

		/// <summary>Create a stream representing a general packet.</summary>
		/// <param name="outStr">Output stream to write to.</param>
		/// <param name="tag">Packet tag.</param>
		/// <param name="length">Size of chunks making up the packet.</param>
		/// <param name="oldFormat">If true, the header is written out in old format.</param>
		public BcpgOutputStream(
			Stream		outStr,
			PacketTag	tag,
			long		length,
			bool		oldFormat)
		{
			if (outStr == null)
				throw new ArgumentNullException("outStr");

			this.outStr = outStr;

			if (length > 0xFFFFFFFFL)
			{
				this.WriteHeader(tag, false, true, 0);
				this.partialBufferLength = 1 << BufferSizePower;
				this.partialBuffer = new byte[partialBufferLength];
				this.partialPower = BufferSizePower;
				this.partialOffset = 0;
			}
			else
			{
				this.WriteHeader(tag, oldFormat, false, length);
			}
		}

		/// <summary>Create a new style partial input stream buffered into chunks.</summary>
		/// <param name="outStr">Output stream to write to.</param>
		/// <param name="tag">Packet tag.</param>
		/// <param name="length">Size of chunks making up the packet.</param>
		public BcpgOutputStream(
			Stream		outStr,
			PacketTag	tag,
			long		length)
		{
			if (outStr == null)
				throw new ArgumentNullException("outStr");

			this.outStr = outStr;
			this.WriteHeader(tag, false, false, length);
		}

		/// <summary>Create a new style partial input stream buffered into chunks.</summary>
		/// <param name="outStr">Output stream to write to.</param>
		/// <param name="tag">Packet tag.</param>
		/// <param name="buffer">Buffer to use for collecting chunks.</param>
		public BcpgOutputStream(
			Stream		outStr,
			PacketTag	tag,
			byte[]		buffer)
		{
			if (outStr == null)
				throw new ArgumentNullException("outStr");

			this.outStr = outStr;
			this.WriteHeader(tag, false, true, 0);

			this.partialBuffer = buffer;

			uint length = (uint) partialBuffer.Length;
			for (partialPower = 0; length != 1; partialPower++)
			{
				length >>= 1;
			}

			if (partialPower > 30)
			{
				throw new IOException("Buffer cannot be greater than 2^30 in length.");
			}
			this.partialBufferLength = 1 << partialPower;
			this.partialOffset = 0;
		}

		private void WriteNewPacketLength(
			long bodyLen)
		{
			if (bodyLen < 192)
			{
				outStr.WriteByte((byte)bodyLen);
			}
			else if (bodyLen <= 8383)
			{
				bodyLen -= 192;

				outStr.WriteByte((byte)(((bodyLen >> 8) & 0xff) + 192));
				outStr.WriteByte((byte)bodyLen);
			}
			else
			{
				outStr.WriteByte(0xff);
				outStr.WriteByte((byte)(bodyLen >> 24));
				outStr.WriteByte((byte)(bodyLen >> 16));
				outStr.WriteByte((byte)(bodyLen >> 8));
				outStr.WriteByte((byte)bodyLen);
			}
		}

		private void WriteHeader(
			PacketTag	tag,
			bool		oldPackets,
			bool		partial,
			long		bodyLen)
		{
			int hdr = 0x80;

			if (partialBuffer != null)
			{
				PartialFlush(true);
				partialBuffer = null;
			}

			if (oldPackets)
			{
				hdr |= ((int) tag) << 2;

				if (partial)
				{
					this.WriteByte((byte)(hdr | 0x03));
				}
				else
				{
					if (bodyLen <= 0xff)
					{
						this.WriteByte((byte) hdr);
						this.WriteByte((byte)bodyLen);
					}
					else if (bodyLen <= 0xffff)
					{
						this.WriteByte((byte)(hdr | 0x01));
						this.WriteByte((byte)(bodyLen >> 8));
						this.WriteByte((byte)(bodyLen));
					}
					else
					{
						this.WriteByte((byte)(hdr | 0x02));
						this.WriteByte((byte)(bodyLen >> 24));
						this.WriteByte((byte)(bodyLen >> 16));
						this.WriteByte((byte)(bodyLen >> 8));
						this.WriteByte((byte)bodyLen);
					}
				}
			}
			else
			{
				hdr |= 0x40 | (int) tag;
				this.WriteByte((byte) hdr);

				if (partial)
				{
					partialOffset = 0;
				}
				else
				{
					this.WriteNewPacketLength(bodyLen);
				}
			}
		}

		private void PartialFlush(
			bool isLast)
		{
			if (isLast)
			{
				WriteNewPacketLength(partialOffset);
				outStr.Write(partialBuffer, 0, partialOffset);
			}
			else
			{
				outStr.WriteByte((byte)(0xE0 | partialPower));
				outStr.Write(partialBuffer, 0, partialBufferLength);
			}

			partialOffset = 0;
		}

		private void WritePartial(
			byte b)
		{
			if (partialOffset == partialBufferLength)
			{
				PartialFlush(false);
			}

			partialBuffer[partialOffset++] = b;
		}

		private void WritePartial(
			byte[]	buffer,
			int		off,
			int		len)
		{
			if (partialOffset == partialBufferLength)
			{
				PartialFlush(false);
			}

			if (len <= (partialBufferLength - partialOffset))
			{
				Array.Copy(buffer, off, partialBuffer, partialOffset, len);
				partialOffset += len;
			}
			else
			{
				int diff = partialBufferLength - partialOffset;
				Array.Copy(buffer, off, partialBuffer, partialOffset, diff);
				off += diff;
				len -= diff;
				PartialFlush(false);
				while (len > partialBufferLength)
				{
					Array.Copy(buffer, off, partialBuffer, 0, partialBufferLength);
					off += partialBufferLength;
					len -= partialBufferLength;
					PartialFlush(false);
				}
				Array.Copy(buffer, off, partialBuffer, 0, len);
				partialOffset += len;
			}
		}
		public override void WriteByte(
			byte value)
		{
			if (partialBuffer != null)
			{
				WritePartial(value);
			}
			else
			{
				outStr.WriteByte(value);
			}
		}
		public override void Write(
			byte[]	buffer,
			int		offset,
			int		count)
		{
			if (partialBuffer != null)
			{
				WritePartial(buffer, offset, count);
			}
			else
			{
				outStr.Write(buffer, offset, count);
			}
		}

		// Additional helper methods to write primitive types
		internal virtual void WriteShort(
			short n)
		{
			this.Write(
				(byte)(n >> 8),
				(byte)n);
		}
		internal virtual void WriteInt(
			int n)
		{
			this.Write(
				(byte)(n >> 24),
				(byte)(n >> 16),
				(byte)(n >> 8),
				(byte)n);
		}
		internal virtual void WriteLong(
			long n)
		{
			this.Write(
				(byte)(n >> 56),
				(byte)(n >> 48),
				(byte)(n >> 40),
				(byte)(n >> 32),
				(byte)(n >> 24),
				(byte)(n >> 16),
				(byte)(n >> 8),
				(byte)n);
		}

		public void WritePacket(
			ContainedPacket p)
		{
			p.Encode(this);
		}

		internal void WritePacket(
			PacketTag	tag,
			byte[]		body,
			bool		oldFormat)
		{
			this.WriteHeader(tag, oldFormat, false, body.Length);
			this.Write(body);
		}

		public void WriteObject(
			BcpgObject bcpgObject)
		{
			bcpgObject.Encode(this);
		}

		public void WriteObjects(
			params BcpgObject[] v)
		{
			foreach (BcpgObject o in v)
			{
				o.Encode(this);
			}
		}

		/// <summary>Flush the underlying stream.</summary>
		public override void Flush()
		{
			outStr.Flush();
		}

		/// <summary>Finish writing out the current packet without closing the underlying stream.</summary>
		public void Finish()
		{
			if (partialBuffer != null)
			{
				PartialFlush(true);
				partialBuffer = null;
			}
		}

		public override void Close()
		{
			this.Finish();
			outStr.Flush();
			outStr.Close();
			base.Close();
		}
	}
}
